﻿namespace ePubReader
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Xml.Linq;

    internal class PackageReader
    {
        public static Package Read(string xml)
        {
            XElement opf = XElement.Parse(xml);
            XAttribute xmlns = opf.Attribute("xmlns");
            XNamespace xNamespace = xmlns != null ? xmlns.Value : opf.Name.Namespace;

            XAttribute versionAttr = opf.Attribute("version");

            double version = 2.0;
            if (versionAttr != null && double.TryParse(versionAttr.Value, out version) == false)
            {
                version = 2.0;
            }

            Metadata meta = LoadMetadata(opf, xNamespace);
            Item[] mani = LoadManifest(opf, xNamespace);
            ItemRef[] spine = LoadSpine(opf, xNamespace);
            Guide guide = LoadGuide(opf, xNamespace);

            return new Package(version, meta, mani, spine, guide);
        }

        static Metadata LoadMetadata(XElement opf, XNamespace xNamespace)
        {
            Metadata metadata = new Metadata()
            {
                Language = "en",
                UniqueIdentifier = "unique-identifier"
            };

            IEnumerable<XElement> metadataElements = opf.Element(xNamespace + "metadata").Elements();

            XAttribute uuidAttr = opf.Attribute("unique-identifier");
            if (uuidAttr != null)
            {
                string uniqueIdentifier = uuidAttr.Value;
                XElement idElement = metadataElements.FirstOrDefault(e => e.Name.LocalName == "identifier" && e.Attribute("id") != null && e.Attribute("id").Value == uniqueIdentifier);
                if (idElement != null)
                {
                    metadata.UniqueIdentifier = idElement.Value;
                }
            }

            foreach (var meta in metadataElements)
            {
                string val = System.Net.HttpUtility.HtmlDecode(meta.Value).Trim();
                if (string.IsNullOrEmpty(val) && meta.Name.LocalName != "meta")
                {
                    continue;
                }

                switch (meta.Name.LocalName)
                {
                    case "title": metadata.Title = val;
                        break;
                    case "creator":
                        var c = new Creator { Role = "aut", Name = val };

                        if (meta.Attribute("role") != null)
                        {
                            c.Role = meta.Attribute("role").Value;
                        }

                        if (meta.Attribute("file-as") != null)
                        {
                            c.FileAs = meta.Attribute("file-as").Value;
                        }

                        if (metadata.Creators == null)
                        {
                            metadata.Creators = new[] { c };
                        }
                        else
                        {
                            metadata.Creators = metadata.Creators.Union(new[] { c }).ToArray();
                        }
                        break;
                    case "identifier":
                        metadata.Identifier = val;
                        break;
                    case "language":
                        metadata.Language = val;
                        break;
                    case "date":
                        Date date = new Date();
                        date.DateTime = val;
                        var attribute = meta.Attributes().FirstOrDefault(a => a.Name.LocalName == "event");
                        if (attribute != null)
                        {
                            date.Name = attribute.Value;
                        }
                        else
                        {
                            date.Name = "publication";
                        }

                        if (metadata.Dates == null)
                        {
                            metadata.Dates = new[] { date };
                        }
                        else
                        {
                            metadata.Dates = metadata.Dates.Union(new[] { date }).ToArray();
                        }

                        break;
                    case "meta":
                        foreach (XAttribute attr in meta.Attributes())
                        {
                            if (attr.Name.LocalName == "property" && attr.Value == "dcterms:modified")
                            {
                                Date mdate = new Date();
                                mdate.Name = "modification";
                                mdate.DateTime = val;
                                if (metadata.Dates == null)
                                {
                                    metadata.Dates = new[] { mdate };
                                }
                                else
                                {
                                    metadata.Dates = metadata.Dates.Union(new[] { mdate }).ToArray();
                                }

                                break;
                            }
                            else if (attr.Name.LocalName == "name" && attr.Value == "cover")
                            {
                                var coverContent = meta.Attribute("content");
                                if (coverContent != null)
                                {
                                    metadata.CoverContent = coverContent.Value;
                                }

                                break;
                            }
                        }
                        break;
                    case "publisher":
                        if (metadata.Publishers == null)
                        {
                            metadata.Publishers = new[] { val };
                        }
                        else
                        {
                            metadata.Publishers = metadata.Publishers.Union(new[] { val }).ToArray();
                        }
                        break;
                    case "subject":
                        if (metadata.Subjects == null)
                        {
                            metadata.Subjects = new[] { val };
                        }
                        else
                        {
                            metadata.Subjects = metadata.Subjects.Union(new[] { val }).ToArray();
                        }
                        break;
                    case "source":
                        if (metadata.Sources == null)
                        {
                            metadata.Sources = new[] { val };
                        }
                        else
                        {
                            metadata.Sources = metadata.Sources.Union(new[] { val }).ToArray();
                        }
                        break;
                    case "rights":
                        metadata.Rights = val;
                        break;
                    case "description":
                        if (metadata.Descriptions == null)
                        {
                            metadata.Descriptions = new[] { val };
                        }
                        else
                        {
                            metadata.Descriptions = metadata.Descriptions.Union(new[] { val }).ToArray();
                        }
                        break;
                    case "contributor":
                        if (metadata.Contributors == null)
                        {
                            metadata.Contributors = new[] { new Creator { Name = val } };
                        }
                        else
                        {
                            metadata.Contributors = metadata.Contributors.Union(new[] { new Creator { Name = val } }).ToArray();
                        }
                        break;
                    case "type":
                        metadata.Type = val;
                        break;
                    case "format":
                        metadata.Format = val;
                        break;
                    case "relation":
                        metadata.Relation = val;
                        break;
                    case "coverage":
                        metadata.Coverage = val;
                        break;
                }
            }

            return metadata;
        }

        static Item[] LoadManifest(XElement opf, XNamespace xNamespace)
        {
            List<Item> items = new List<Item>();
            IEnumerable<XElement> manifestElements = opf.Element(xNamespace + "manifest").Elements();

            foreach (XElement xItem in manifestElements)
            {
                Item item = new Item()
                {
                    Properties = new string[0]
                };

                foreach (XAttribute attr in xItem.Attributes())
                {
                    string val = System.Net.HttpUtility.HtmlDecode(attr.Value);
                    switch (attr.Name.LocalName)
                    {
                        case "id":
                            item.Id = val;
                            break;
                        case "href":
                            item.Href = val;
                            break;
                        case "media-type":
                            item.MediaType = val;
                            break;
                        case "fallback":
                            item.Fallback = val;
                            break;
                        case "properties":
                            item.Properties = val.Split(new[] { ' ' });
                            break;
                    }
                }

                items.Add(item);
            }

            return items.ToArray();
        }

        static ItemRef[] LoadSpine(XElement opf, XNamespace xNamespace)
        {
            Dictionary<string, ItemRef> spine = new Dictionary<string, ItemRef>();
            IEnumerable<XElement> spineElements = opf.Element(xNamespace + "spine").Elements();

            foreach (XElement xItem in spineElements)
            {
                ItemRef item = new ItemRef()
                {
                    Linear = true,
                    Id = string.Empty,
                    Properties = new string[0]
                };

                foreach (XAttribute attr in xItem.Attributes())
                {
                    string val = System.Net.HttpUtility.HtmlDecode(attr.Value);
                    switch (attr.Name.LocalName)
                    {
                        case "idref":
                            item.Idref = val;
                            break;
                        case "id":
                            item.Id = val;
                            break;
                        case "linear":
                            if (val == "no")
                            {
                                item.Linear = false;
                            }
                            break;
                        case "properties":
                            item.Properties = val.Split(new[] { ' ' });
                            break;
                    }
                }

                if (spine.ContainsKey(item.Idref) == false)
                {
                    spine.Add(item.Idref, item);
                }
            }

            return spine.Select(s => s.Value).ToArray();
        }

        static Guide LoadGuide(XElement opf, XNamespace xNamespace)
        {
            Guide guide = new Guide();
            var gElement = opf.Element(xNamespace + "guide");
            if (gElement != null)
            {
                foreach (XElement xItem in gElement.Elements())
                {
                    Reference reference = new Reference();
                    foreach (XAttribute attr in xItem.Attributes())
                    {
                        string val = System.Net.HttpUtility.HtmlDecode(attr.Value);
                        switch (attr.Name.LocalName)
                        {
                            case "type":
                                reference.Type = val;
                                break;
                            case "title":
                                reference.Title = val;
                                break;
                            case "href":
                                reference.Href = val;
                                break;
                        }
                    }

                    guide[reference.Type] = reference;
                }
            }

            return guide;
        }
    }
}
